<?php
class Validator extends IError
{
	/** Validation fields configuration
	 * @var array
	 */
	var $fields = array();

	/** Validation rules
	 * @var array
	 */
	var $rules = array();  // 0 - first op, 1 - second op, 2 - operation


	/** Callback function for whole form rules
	 * @var array
	 */
	var $callback = array();


	/** Constuctor.
	 * @param array $fields array of field validation schemes
	 * @param array $rules validation rules
	 */
	function Validator($fields, $rules=array(), $callback=array() )
	{
		$this->setFields($fields);
		$this->rules	= $rules;
		$this->callback = $callback;
	}

	/** Checks if capcha is valid
	 * @param string $sIp
	 * @return boolean result
	 */
	public static function isCapcha($capcha == false){
		$capcha = strtolower($capcha);
		$pstring = strtolower($_SESSION['Pstring']);
		return ($capcha == $pstring and $capcha)? true : false ;
	}

	function setFields($fields)
	{
		// very important (!) for debug
		$aKeys = array('def', 'title', 'pattern', 'optional', 'minlen', 'maxlen', 'min', 'max', 'mineq', 'maxeq', 'callback', 'message', 'maxsize', 'minsize');
		foreach ($fields as $k => $scheme)
		{
			if (isset($scheme['def']))
			{
				$scheme = array_merge($this->_getDef($scheme['def'], $scheme['title']), $scheme);
				unset($scheme['def']);
			}

			if (array_diff(array_keys($scheme), $aKeys))
				IError::fatal('validator.unknown_keys');

			$this->fields[$k] = $scheme;
		}
	}
	/** Returns specified value from a given array. supports one [] in names
	 * @param string $k	name of variable
	 * @param string $values values array (_POST)
	 * @access private
	 * @return string
	 */
	function _getValue($k, &$values)
	{
		// vitally important for debug ( do not use isset (!) )
		$val = '';
		if ( preg_match('/^(.+)\[(.+)\]\[(.+)\]$/', $k, $mat) ){
			$val = $values[$mat[1]][$mat[2]][$mat[3]];
		}elseif ( preg_match('/^(.+)\[(.+)\]$/', $k, $mat) ){
			$val = $values[$mat[1]][$mat[2]];
		}elseif ( preg_match('/^(.+)\[\]$/', $k, $mat) )
			$val = isset($values[$mat[1]])? $values[$mat[1]] : array();
		else
			$val = (array_key_exists($k, $values)) ? $values[$k] : "";

		return $val;
	}

	/** Checks whether data is valid according to rules
	 *
	 * @param array $values values to check
	 * @return boolean true - values are valid, false - invalid values
	 */
	function isValid($values)
	{
		//single fields
		foreach ($this->fields as $field => $scheme)
			$this->_checkField($this->_getValue($field, $values), $scheme);
		//rules
		foreach ($this->rules as $rule)
			$this->_checkRule($rule, $values);

		//callback
		foreach ($this->callback as $callback)
		{
			$err = call_user_func($callback[0], $values);
			$this->addErrors($err);
		}
		return (0 == sizeof($this->_errors));
	}


	function _checkRule($rule, $values)
	{
		$op1 = $this->_getValue($rule[0], $values);
		$op2 = $this->_getValue($rule[1], $values);
		$isError = false;
		switch ($rule[2])
		{
			case '==':
				$isError = !($op1 == $op2);
			break;
			case '!=':
				$isError = !($op1 != $op2);
			break;
			case '<=':
				$isError = (($op1!='') && ($op2!='') && !($op1 <= $op2));
			break;
			case '>=':
				$isError = (($op1!='') && ($op2!='') && !($op1 >= $op2));
			break;
			case 'req':
				$isError = ($op1 and !$op2);
			break;

			case '>':
				$isError = (($op1!='') && ($op2!='') && !($op1 > $op2));
			break;
			case '<':
				$isError = (($op1!='') && ($op2!='') && !($op1 < $op2));
			break;

			case 'more':
				$isError = (sizeof($op1) < sizeof($op2));
			break;
			case 'less':
				$isError = (sizeof($op1) > sizeof($op2));
			break;

			default:
				IError::fatal('validator.unknown_rule: '. $rule[2]);
		}// switch

		if ($isError)
			$this->addError('validator.rule.'.$rule[2], array(
				$this->fields[$rule[0]]['title'],
				$this->fields[$rule[1]]['title'])
			);
	}

	/** Constructs javascript for client-side validation
	 * @param string $form name of HTML form to validate
	 * @param boolean $isReturn sets if returned string contains 'return' statement
	 * @param string $sDivName Id of HTML div for error output. if empty js uses alert()
	 * @return string javascript code for validation
	 */
	function makeJS($form = 'this', $isReturn=true, $divName='jsErr')
	{
		$script	= 'new Array( ';
		// fields
		foreach ($this->fields as $field => $scheme )
		{
			$script .= "{'field':'".$field."'";
			foreach ($scheme as  $k => $val)
			{
				if ($k == 'callback')
					$val = $val[0];
				if (in_array($k, array('title','message')))
					$val = "'".$val."'";
				$script .= ",'".$k."':".$val;
			}
			$script .= '},';
		}
		//rules
		$script = substr($script, 0, -1).'), new Array( ';
		foreach ($this->rules as $rule)
			$script .= sprintf("['%s','%s','%s','%s'],", $rule[0], $rule[1], $rule[2],
				_fmt('validator.rule.'.$rule[2], array($this->fields[$rule[0]]['title'], $this->fields[$rule[1]]['title'])));
		$script = substr($script, 0, -1).'),';

		//calbacks
		$script = substr($script, 0, -1).', new Array( ';
		foreach ($this->callback as $callback)
			$script .= $callback[1].",";
		$script = substr($script, 0, -1).')';

		$script =  " validator_isValid($form, $script, '$divName')";
		if ($isReturn)
			$script  = 'return '.$script;
		return $script;
	}

	/** Make javascript with messages array.
	 * @return string javascript code (with <script> tags) for messages array
	 */
	function makeJsMess()
	{
		require ('config/messages.php');
		$script = 'var aValMes = {};';
		foreach ($msg as $k=>$v)
		{
			$name = str_replace('validator.field.', '', $k);
			if ($name != $k)
				$script .= " aValMes['$name']  = '"._fmt('validator.field.'.$name, array('%s0', '%s1', '%s2', '%s3', '%s4'))."';";
		}
		return $script;
	}


	/** Validates single field.
	 * @param mixed $mVal
	 * @param array $scheme
	 * @access private
	 * @returns boolean
	 */
	function _checkField($mValue, $scheme)
	{
		$isEmpty = ((is_string($mValue) and strlen($mValue)==0) or
				   (is_array($mValue)) and sizeof($mValue)==0);

		if ( isset($scheme['optional']) && $scheme['optional'] && $isEmpty)
			return true;

		$value = is_array($mValue)? $mValue : array($mValue);
		$err = array();

		// check size
		if ( isset($scheme['minsize']) and sizeof($value) < $scheme['minsize'])
			$err['minsize'] = array($scheme['title'], $scheme['minsize'], sizeof($value));

		if ( isset($scheme['maxsize']) and sizeof($value) > $scheme['maxsize'])
			$err['maxsize'] = array($scheme['title'], $scheme['maxsize'], sizeof($value));

		foreach($value as $val)
		{
			if ( isset($scheme['minlen']) and strlen($val) < $scheme['minlen'])
				$err['minlen'] = array($scheme['title'], $scheme['minlen'], strlen($val));

			if ( isset($scheme['maxlen']) and strlen($val) > $scheme['maxlen'])
				$err['maxlen'] =  array($scheme['title'], $scheme['maxlen'], strlen($val));

			if ( isset($scheme['pattern']) and !preg_match($scheme['pattern'], $val) )
				$err['pattern'] = array($scheme['title'], $scheme['pattern'], $val );

			if ( isset($scheme['min']) and $val < $scheme['min'])
				$err['min'] = array($scheme['title'], $scheme['min'], $val);

			if ( isset($scheme['max']) and $val > $scheme['max'])
				$err['max'] = array($scheme['title'], $scheme['max'], $val);

			if ( isset($scheme['mineq']) and $val <= $scheme['mineq'])
				$err['mineq'] = array($scheme['title'], $scheme['mineq'], $val);

			if ( isset($scheme['maxeq']) and $val >= $scheme['maxeq'])
				$err['maxeq'] = array($scheme['title'], $scheme['maxeq'], $val);
			//if ( isset($scheme['enum']) and !in_array($val, $scheme['enum']))
			//	$err['enum'] =  array($scheme['title'], $val);
			if (isset($scheme['callback']))
			{
				if (! call_user_func($scheme['callback'][1], $val))
					$err['pattern'] = array($scheme['title'], $val);
			}
			if ($err) // find firs field with error and stop checks
				break;
		}

		if (! $err)
			return true;

		if (isset($scheme['message']))
			$this->addError('validator.field.custom', $scheme['message']);
		else
		{
			foreach ($err as $k=>$param)
				$this->addError('validator.field.'.$k, $param);
		}
		return false;
	}

	/** Gets default validation schemes by key. If unknown rule throws exeption
	 * @access private
	 * @param string $k
	 * @return array scheme
	 */
	function _getDef($k, $title = '')
	{
		$validatorDefs  = array(
			'email' => array(
				'pattern'=>'/^[0-9a-zA-Z]([\-.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][\-0-9a-zA-Z]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,}$/',
				'maxlen'=>255,
			),
			'pass' => array(
				'pattern'=>'/^[0-9a-zA-Z\*]+$/',
				'minlen'=>8,
				'maxlen'=>20,
			),
			'ip' => array(
				'callback' => array('validator_isIp', array('Validator','isIp')),
				'pattern'  => '/^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/',
			),
			'url' => array(
				'pattern' => '/^(?:(ftp|http|https):)?(?:\/\/(?:((?:%[0-9a-f]{2}|[\-a-z0-9_.!~*\'\(\);:&=\+\$,])+)@)?(?:((?:[a-z0-9](?:[\-a-z0-9]*[a-z0-9])?\.)*[a-z](?:[\-a-z0-9]*[a-z0-9])?)|([0-9]{1,3}(?:\.[0-9]{1,3}){3}))(?::([0-9]*))?)?((?:\/(?:%[0-9a-f]{2}|[\-a-z0-9_.!~*\'\(\):@&=\+\$,;])+)+)?\/?(?:\?.*)?$/i',
				'maxlen'=> 255,
				'minlen'=> 11
			),

			'login' => array(
				'pattern'=>'/^[\w\-\.]+$/',
				'minlen'=>8,
				'maxlen'=>16
			),
			'zip'  => array(
				'pattern'=>'/^[a-zA-Z\d\-\s]{1,10}$/',
			),
			'zip_us'  => array(
				'pattern'=>'/^\d{5}(-\d{4})?$/',
			),
			'zip_canada'  => array(
				'pattern'=>'/^[a-zA-Z\d]{6}$/',
			),
			'phone_us'  => array(
				'minlen'=>12,
				'pattern'=>'/^(1-?)?(\([2-9]\d{2}\)|[2-9]\d{2})-?[2-9]\d{2}-?\d{4}$/',
			),
			'phone'  => array(
				'minlen'=> 1,
				'pattern'=>'/^[\d\+\(\)\s\-]+$/',
			),
			'money' => array(
				'maxlen'=>12,
				'pattern'=>'/^\d+(\.\d\d)?$/',
			),
			'cvv'  => array(
				'pattern'=>'/^\d{3,4}$/',
			),
			'ssn'  => array(
				'maxlen' =>11,
				'pattern'=>'/^([\d]{3}-[\d]{2}-[\d]{4})|([\d]{2}-[\d]{7})$/',
			),
			'integer' => array(
				'maxlen'=>20,
				'pattern'=>'/^\d+$/',
			),
			'number' => array(
				'maxlen'=>20,
				'pattern'=>'/^\d+$/',
			),
			'float' => array(
				'maxlen'=>20,
				'pattern'=>'/^\d+(\.\d+)?$/',
			),
			'cc_visa' => array(
				'maxlen'=>16,
				'pattern'=>'/^4(\d{12}|\d{15})$/',
			),
			'cc_master' => array(
				'maxlen'=>16,
				'pattern'=>'/^5[1-5]\d{14}$/',
			),
			'cc_ae' => array(
				'maxlen'=>15,
				'pattern'=>'/^3[47]\d{13}$/',
			),
			'cc_discover' => array(
				'maxlen'=>16,
				'pattern'=>'/^6011\d{12}$/',
			),
			'cc_diners' => array(
				'maxlen'=>14,
				'pattern'=>'/^3(0[0-5]|[68][0-9])\d{11}$/',
			),
			'cc_jcb' => array(
				'maxlen'=>16,
				'pattern'=>'/^(3\d{4}|2131|1800)\d{11}$/',
			),
			'cc_enroute' => array(
				'maxlen'=>15,
				'pattern'=>'/^2(014|149)\d{11}$/',
			),
			'required'  => array(
				'minlen' =>1,
			),
			'percent'   => array(
				'minlen'=>2,
				'pattern' => '/^([0-9]|[1-9][0-9]|100)$/'
			),
			'bool'   => array(
				'pattern' => '/^[0-1]?$/'
			),
		);
		if (! array_key_exists($k, $validatorDefs))
			IError::fatal('validator.unknown_def: '. $k);

		if (in_array($k, array('required', 'ssn', 'ip')))
			$validatorDefs[$k]['message'] = _fmt('validator.def.'.$k, array($title));

		return $validatorDefs[$k];
	}

	/** Checks if ip is in valid range
	 * @param string $sIp
	 * @return boolean result
	 */
	function isIp($ip)
	{
		return ('0.0.0.0' != $ip && '255.255.255.255' != $ip);
	}
}